﻿// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if NETFRAMEWORK
using System.Data;
using System.Data.Common;
#endif

using System.ComponentModel;

namespace Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Used to store information that is provided to unit tests.
/// </summary>
public abstract class TestContext
{
    internal static readonly string FullyQualifiedTestClassNameLabel = nameof(FullyQualifiedTestClassName);
    internal static readonly string TestNameLabel = nameof(TestName);
#if !WINDOWS_UWP && !WIN_UI
    internal static readonly string TestRunDirectoryLabel = nameof(TestRunDirectory);
    internal static readonly string DeploymentDirectoryLabel = nameof(DeploymentDirectory);
    internal static readonly string ResultsDirectoryLabel = nameof(ResultsDirectory);
    internal static readonly string TestRunResultsDirectoryLabel = nameof(TestRunResultsDirectory);
    internal static readonly string TestResultsDirectoryLabel = nameof(TestResultsDirectory);
#endif

    /// <summary>
    /// Gets test properties for a test.
    /// </summary>
    public abstract IDictionary<string, object?> Properties { get; }

    /// <summary>
    /// Gets or sets the cancellation token source. This token source is canceled when test times out. Also when explicitly canceled the test will be aborted.
    /// </summary>
    // Disposing isn't important per https://github.com/dotnet/runtime/issues/29970#issuecomment-717840778
    [EditorBrowsable(EditorBrowsableState.Never)]
    public virtual CancellationTokenSource CancellationTokenSource { get; protected internal set; } = new();

    /// <summary>
    /// Gets the cancellation token. This token is canceled when test times out. Also when explicitly canceled the test will be aborted.
    /// </summary>
    public CancellationToken CancellationToken => CancellationTokenSource.Token;

    /// <summary>
    /// Gets or sets the test data for the test method being executed.
    /// </summary>
    public object?[]? TestData { get; protected set; }

    /// <summary>
    /// Gets or sets the test display name for the test method being executed.
    /// </summary>
    public string? TestDisplayName { get; protected set; }

#if NETFRAMEWORK
    /// <summary>
    /// Gets the current data row when test is used for data driven testing.
    /// </summary>
    public abstract DataRow? DataRow { get; }

    /// <summary>
    /// Gets current data connection row when test is used for data driven testing.
    /// </summary>
    public abstract DbConnection? DataConnection { get; }
#endif

#if !WINDOWS_UWP && !WIN_UI
    #region Test run deployment directories

    /// <summary>
    /// Gets base directory for the test run, under which deployed files and result files are stored.
    /// </summary>
    public virtual string? TestRunDirectory => GetProperty<string>(TestRunDirectoryLabel);

    /// <summary>
    /// Gets directory for files deployed for the test run. Typically a subdirectory of <see cref="TestRunDirectory"/>.
    /// </summary>
    public virtual string? DeploymentDirectory => GetProperty<string>(DeploymentDirectoryLabel);

    /// <summary>
    /// Gets base directory for results from the test run. Typically a subdirectory of <see cref="TestRunDirectory"/>.
    /// </summary>
    public virtual string? ResultsDirectory => GetProperty<string>(ResultsDirectoryLabel);

    /// <summary>
    /// Gets directory for test run result files. Typically a subdirectory of <see cref="ResultsDirectory"/>.
    /// </summary>
    public virtual string? TestRunResultsDirectory => GetProperty<string>(TestRunResultsDirectoryLabel);

    /// <summary>
    /// Gets directory for test result files.
    /// </summary>
    // In MSTest, it is actually "In\697105f7-004f-42e8-bccf-eb024870d3e9\User1", but we are setting it to "In" only
    // because MSTest does not create the GUID directory.
    public virtual string? TestResultsDirectory => GetProperty<string>(TestResultsDirectoryLabel);

    #endregion
#endif

    /// <summary>
    /// Gets the Fully-qualified name of the class containing the test method currently being executed.
    /// </summary>
    public virtual string FullyQualifiedTestClassName => GetProperty<string>(FullyQualifiedTestClassNameLabel)
        ?? throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, FrameworkMessages.InvalidAccessToTestContextProperty, nameof(FullyQualifiedTestClassName)));

    /// <summary>
    /// Gets the name of the test method currently being executed.
    /// </summary>
    public virtual string TestName => GetProperty<string>(TestNameLabel)
        ?? throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, FrameworkMessages.InvalidAccessToTestContextProperty, nameof(TestName)));

    /// <summary>
    /// Gets the current test outcome.
    /// </summary>
    public virtual UnitTestOutcome CurrentTestOutcome => UnitTestOutcome.Unknown;

    /// <summary>
    /// Gets or sets the exception that occurred in the TestInitialize or TestMethod steps.
    /// </summary>
    /// <remarks>
    /// The property is always <c>null</c> when accessed during the TestInitialize or TestMethod steps.
    /// </remarks>
    public Exception? TestException { get; protected set; }

    /// <summary>
    /// Gets the current attempt of the test run. This property is relevant when
    /// using <see cref="RetryAttribute"/> (or any implementation of <see cref="RetryBaseAttribute"/>).
    /// On the first run, this property is set to 1.
    /// On subsequent retries, the value is incremented.
    /// </summary>
    public int TestRunCount { get; internal set; }

    /// <summary>
    /// Adds a file name to the list in TestResult.ResultFileNames.
    /// </summary>
    /// <param name="fileName">
    /// The file Name.
    /// </param>
    public abstract void AddResultFile(string fileName);

    /// <summary>
    /// Used to write trace messages while the test is running.
    /// </summary>
    /// <param name="message">formatted message string.</param>
    public abstract void Write(string? message);

    /// <summary>
    /// Used to write trace messages while the test is running.
    /// </summary>
    /// <param name="format">format string.</param>
    /// <param name="args">the arguments.</param>
    public abstract void Write(string format, params object?[] args);

    /// <summary>
    /// Used to write trace messages while the test is running.
    /// </summary>
    /// <param name="message">formatted message string.</param>
    public abstract void WriteLine(string? message);

    /// <summary>
    /// Used to write trace messages while the test is running.
    /// </summary>
    /// <param name="format">format string.</param>
    /// <param name="args">the arguments.</param>
    public abstract void WriteLine(string format, params object?[] args);

    /// <summary>
    /// Used to write trace messages while the test is running.
    /// </summary>
    /// <param name="messageLevel">The message level.</param>
    /// <param name="message">The message.</param>
    public abstract void DisplayMessage(MessageLevel messageLevel, string message);

    private T? GetProperty<T>(string name)
        where T : class
    {
        DebugEx.Assert(Properties is not null, "Properties is null");
#if WINDOWS_UWP || WIN_UI
        if (!Properties.TryGetValue(name, out object? propertyValue))
        {
            return null;
        }
#else
        // This old API doesn't throw when key is not found, but returns null.
        object? propertyValue = Properties[name];
#endif

        // If propertyValue has a value, but it's not the right type
        if (propertyValue is not null and not T)
        {
            Debug.Fail("How did an invalid value get in here?");
            throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.InvalidPropertyType, name, propertyValue.GetType(), typeof(T)));
        }

        return (T?)propertyValue;
    }
}
